package it.ap.sa.core.utils;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.Temporal;
import java.util.Date;
import java.util.GregorianCalendar;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;


//
public class DateTimeUtil {

	private static final Logger logger = LoggerFactory.getLogger(DateTimeUtil.class);
	
/** ============================================ FORMAT DATE ============================================= **/

	public static final String YYYYMMDD = "yyyyMMdd";

	public static final String DDMMYYYY_Slash = "dd/MM/yyyy";
	public static final String DDMMYYYY_HHmm_Slash = "dd/MM/yyyy HH:mm";
	public static final String DDMMYYYY_HHmmss_Slash = "dd/MM/yyyy HH:mm:ss";

	public static final String DDMMYYYY = "dd-MM-yyyy";
	public static final String DDMMYYYY_HHmm = "dd-MM-yyyy HH:mm";
	public static final String DDMMYYYY_HHmmss = "dd-MM-yyyy HH:mm:ss";
	
	public static final String YYYY_MM_DD = "yyyy-MM-dd";
	public static final String YYYYMMDD_HHmm = "yyyy-MM-dd HH:mm";
	public static final String YYYYMMDDTHHmm = "yyyy-MM-dd'T'HH:mm";
	public static final String YYYYMMDD_HHmmss = "yyyy-MM-dd HH:mm:ss";
	
	public static final String HHmm = "HH:mm";
	public static final String HHmmss = "HH:mm:ss";
	public static final String HHmmss_SSS = "HH:mm:ss.SSS";
	
	// "Fri Mar 11 12:22:33 CET 2016"
	public static final String DYMONDDHH24MISS_CET_YYYY = "DY MON DD HH24:MI:SS CET YYYY";
	
	public static final String YYYYMMDD_HHMMSS_FILE = "yyyyMMdd_HHmmss";

	/**
	 * @author Alessandro Paolillo
	 */
	@SuppressWarnings("unchecked")
	private static <T extends Temporal> T transformStringToTemporal(String date, String formatDate, Class<T> classe)throws Exception{
		try {
			if (!StringUtils.hasText(date) || !StringUtils.hasText(formatDate)) return null;
			
			DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
			builder.append(DateTimeFormatter.ofPattern(formatDate));
			
			DateTimeFormatter dtf = builder.toFormatter();
			T result = null;
			
			if(classe.equals(LocalDateTime.class)) result = (T)LocalDateTime.parse(date, dtf);
			else 
				if(classe.equals(LocalDate.class)) result = (T)LocalDate.parse(date, dtf);
			else 
				if(classe.equals(LocalTime.class)) result = (T)LocalTime.parse(date, dtf); 
			else
				throw new Exception(classe + " non gestita");
			
			return result;
		} catch (Exception e) {
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
	}
	
/** ============================================ java.util.Date <<<--->>> String ====================================== **/

	/**
	 * Restituisce la data fornita in ingresso come stringa nel formato specificato.
	 * 
	 * @param {@link Date} date
	 * @param {@link String} format
	 * 
	 * @return String
	 */
	public static String transformDateToString(Date date, String format) throws Exception {
		try{
			if (date == null || !StringUtils.hasText(format)) return null;
			
			DateFormat df = new SimpleDateFormat(format);
			return df.format(date);
		}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
	}
	
	/**
	 * Restituisce la data fornita in ingresso come {@link Date} nel formato specificato.
	 * 
	 * @param {@link Date} date
	 * @param {@link String} formatString, formato in entrata del parametro date
	 * @param {@link String} formatDate, formato in uscita della data
	 * 
	 * @return String
	 */
	public static Date transformStringToDate(String date, String formatString, String formatDate) throws Exception {
		try {
			if (!StringUtils.hasText(date) || !StringUtils.hasText(formatString) || !StringUtils.hasText(formatDate)) return null;

			DateFormat df = new SimpleDateFormat(formatDate);
			DateFormat sdf1 = new SimpleDateFormat(formatString);
			
			String d = df.format(sdf1.parse(date));
			Date data = df.parse(d);
			
			return data;
		} catch (Exception e) {
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
	}
	
/** ====================================== Current Date in java.time.*  ===================================== **/

	/**
	 * Restituisca la data/ora corrente (es: 2014-04-28T16:00:49.455)
	 * 
	 * @return LocalDateTime
	 */
	public static LocalDateTime nowAsLocalDateTime() {
		return LocalDateTime.now();
	}
	
	/**
	 * Restituisca la data corrente (es: 2014-04-28)
	 * 
	 * @return LocalDate
	 */
	public static LocalDate nowAsLocalDate() {
		return LocalDate.now();
	}
	
/** ============================================ java.time.LocalDateTime <<<--->>> STRING ====================================== **/

	/**
	 * Restituisce la data fornita in ingresso come stringa nel formato specificato.
	 * 
	 * @param {@link LocalDateTime} date
	 * @param {@link String} format
	 * 
	 * @return String
	 */
	public static String transformLocalDateTimeToString(LocalDateTime date, String format) throws Exception {
		try{
			if (date == null || !StringUtils.hasText(format)) return null;
			
			DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
			
			return formatter.format(date);
		}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
	}
	
	/**
	 * Restituisce la data fornita in ingresso come {@link LocalDateTime}.
	 * 
	 * @param {@link String} date
	 * @param {@link String} formatDate, formato in entrata della data
	 * 
	 * @return LocalDateTime
	 */
	public static LocalDateTime transformStringToLocalDateTime(String date, String formatDate) throws Exception {
		return transformStringToTemporal(date, formatDate, LocalDateTime.class);
	}
	
/** ============================================ java.time.LocalDate <<<--->>> STRING ====================================== **/

	/**
	 * Restituisce la data fornita in ingresso come stringa nel formato specificato.
	 * 
	 * @param {@link LocalDate} date
	 * @param {@link String} format
	 * 
	 * @return String
	 */
	public static String transformLocalDateToString(LocalDate date, String format) throws Exception {
		try{
			if (date == null || !StringUtils.hasText(format)) return null;
			
			return date.format(DateTimeFormatter.ofPattern(format));
		}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
	}
	
	/**
	 * Restituisce la data fornita in ingresso come {@link LocalDate}.
	 * 
	 * @param {@link String} date
	 * @param {@link String} formatDate, formato in entrata della data
	 * 
	 * @return LocalDate
	 */
	public static LocalDate transformStringToLocalDate(String date, String formatDate) throws Exception {
		return transformStringToTemporal(date, formatDate, LocalDate.class);
	}
	
/** ============================================ java.time.LocalTime <<<--->>> STRING ====================================== **/
	
	/**
	 * Restituisce l'ora fornita in ingresso come stringa nel formato specificato.
	 * 
	 * @param {@link LocalTime} time
	 * @param {@link String} format
	 * @return
	 * @throws Exception
	 */
	public static String transformLocalTimeToString(LocalTime time, String format) throws Exception {
		try{
			if (time == null || !StringUtils.hasText(format)) return null;
			
			return time.format(DateTimeFormatter.ofPattern(format));
		}catch(Exception e ){
			logger.error( time + "can't be formatted >>> " + e, e);
			throw(e);
		}
	}
	
	/**
	 * @author Alessandro Paolillo
	 */
	public static LocalTime transformStringToLocalTime(String time, String formatTime) throws Exception {
		return transformStringToTemporal(time, formatTime, LocalTime.class);
	}
	
/** ============================================ java.util.Date <<<--->>> java.time.* ====================================== **/

	/**
	 * @author Alessandro Paolillo
	 */
	public static LocalDate asLocalDate(long timeInMillisecond) {
		return asLocalDate(new java.util.Date(timeInMillisecond));
	}
	/**
     * Crea {@link #asLocalDate(Date, ZoneId)} con il default time zone del sistema.
     */
    public static LocalDate asLocalDate(java.util.Date date) {
    	try{
    		return asLocalDate(date, ZoneId.systemDefault());
    	}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }

    /**
     * Crea {@link LocalDate} da {@code java.util.Date} o sue subclasses.
     */
    public static LocalDate asLocalDate(java.util.Date date, ZoneId zone) {
    	try{
		    if (date == null)
		        return null;
		
		    if (date instanceof java.sql.Date)
		        return ((java.sql.Date) date).toLocalDate();
		    else
		        return Instant.ofEpochMilli(date.getTime()).atZone(zone).toLocalDate();
    	}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }

    /**
     * Crea {@link #asLocalDateTime(Date, ZoneId)} con il default time zone del sistema.
     */
    public static LocalDateTime asLocalDateTime(long utc) {
    	try{
    		return asLocalDateTime(new java.util.Date(utc), ZoneId.systemDefault());
    	}catch(Exception e ){
			logger.error( utc + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }
    
    /**
     * @author Alessandro Paolillo
     */
    public static LocalDateTime asLocalDateTime(LocalDate localDate) throws Exception {
    	return asLocalDateTime(getTimeMillis(localDate));
    }
    
    /**
     * Crea {@link #asLocalDateTime(Date, ZoneId)} con il default time zone del sistema.
     */
    public static LocalDateTime asLocalDateTime(java.util.Date date) {
    	try{
        	return asLocalDateTime(date, ZoneId.systemDefault());
	    }catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }

    /**
     * Crea {@link LocalDateTime} da {@code java.util.Date} o sottoclassi.
     */
    public static LocalDateTime asLocalDateTime(java.util.Date date, ZoneId zone) {
    	try{
	        if (date == null) return null;
	
	        if (date instanceof java.sql.Timestamp)
	            return ((java.sql.Timestamp) date).toLocalDateTime();
	        else
	            return Instant.ofEpochMilli(date.getTime()).atZone(zone).toLocalDateTime();
    	}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }
    
    /**
     * @author Alessandro Paolillo
     */
    public static LocalTime asLocalTime(java.util.Date time, ZoneId zone) {
    	try{
    		if (time == null) return null;
    		
    		if (time instanceof java.sql.Time)
    			return ((java.sql.Time) time).toLocalTime();
    		else
    			return Instant.ofEpochMilli(time.getTime()).atZone(zone).toLocalTime();
    	}catch(Exception e ){
    		logger.error( time + "can't be formatted >>> " + e, e);
    		throw(e);
    	}
    }
    
    /**
     * @author Alessandro Paolillo
     */
    public static LocalTime asLocalTime(java.util.Date time) {
    	try{
        	return asLocalTime(time, ZoneId.systemDefault());
	    }catch(Exception e ){
			logger.error( time + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }

    /**
     * Crea {@link #asUtilDate(Object, ZoneId)} con il default time zone del sistema.
     */
    public static java.util.Date asUtilDate(Object date) {
    	try{
    		return asUtilDate(date, ZoneId.systemDefault());
    	}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }

    /**
     * Crea un {@link java.util.Date} da vari date objects. Supportati:
     * <ul>
     * 	<li>{@link java.util.Date}
     * 	<li>{@link java.sql.Date}
     * 	<li>{@link java.sql.Timestamp}
     * 	<li>{@link java.time.LocalDate}
     * 	<li>{@link java.time.LocalDateTime}
     * 	<li>{@link java.time.ZonedDateTime}
     * 	<li>{@link java.time.Instant}
     * </ul>
     * 
     * @param {{@link ZoneId} zone, usato solo se l'input è un LocalDate o un LocalDateTime.
     * 
     * @return {@link java.util.Date}
     */
    public static java.util.Date asUtilDate(Object date, ZoneId zone) {
    	try{
	        if (date == null) return null;
	
	        if (date instanceof java.sql.Date || date instanceof java.sql.Timestamp) 
	        	return new java.util.Date(((java.util.Date) date).getTime());
	        
	        if (date instanceof java.util.Date) return (java.util.Date) date;
	        
	        if (date instanceof LocalDate)
	            return java.util.Date.from(((LocalDate) date).atStartOfDay(zone).toInstant());
	        
	        if (date instanceof LocalDateTime)
	            return java.util.Date.from(((LocalDateTime) date).atZone(zone).toInstant());
	        
	        if (date instanceof ZonedDateTime) return java.util.Date.from(((ZonedDateTime) date).toInstant());
	        
	        if (date instanceof Instant) return java.util.Date.from((Instant) date);
	
	        throw new UnsupportedOperationException("Don't know how to convert " + date.getClass().getName() + " to java.util.Date");
    	}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }
    
/** =================================================== java.time.Instant =============================================== **/

    /**
     * Crea un {@link Instant} da {@code java.util.Date}.
     */
    public static Instant asInstant(Date date) {
    	try{
	        if (date == null)
	            return null;
	        else
	            return Instant.ofEpochMilli(date.getTime());
    	}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }

/** =================================================== java.time.ZonedDateTime =============================================== **/

    /**
     * Crea {@link #asZonedDateTime(Date, ZoneId)} con il default time zone del sistema.
     */
    public static ZonedDateTime asZonedDateTime(Date date) {
    	try{
    		return asZonedDateTime(date, ZoneId.systemDefault());
    	}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }

    /**
     * Crea un {@link ZonedDateTime} da {@code java.util.Date}
     */
    public static ZonedDateTime asZonedDateTime(Date date, ZoneId zone) {
    	try{
	        if (date == null)
	            return null;
	        else
	            return asInstant(date).atZone(zone);
    	}catch(Exception e ){
			logger.error( date + "can't be formatted >>> " + e, e);
			throw(e);
		}
    }
    
/** ============================================ Integer <<<--->>> String ====================================== **/
    
    /**
	 * Restituisce i secondi forniti in ingresso come {@link String} nel formato hh:mm:ss.
	 * 
	 * @param {@link Integer} seconds
	 * 
	 * @return String
	 */
	public static String transformSecondsIntToStringHHMMSS(int seconds) throws Exception {
		try {
			int secondiGiorno = 86400;
			int numeroGiorni  = 0;
			
			while(seconds > secondiGiorno){
				numeroGiorni++;
				seconds =  seconds - secondiGiorno;
			}
			int hours   = 0;
			int minutes = 0;
			
			hours   = seconds/3600;
			seconds = seconds - hours * 3600;
			hours   = hours + numeroGiorni * 24;
			minutes = seconds/60;
			seconds = seconds - minutes * 60;
			
			
			String result =  hours  <10?"0"+hours   : String.valueOf(hours  );
			result += ":" + (minutes<10?"0"+minutes : String.valueOf(minutes));
			result += ":" + (seconds<10?"0"+seconds : String.valueOf(seconds));
			
			return result;
		} catch (Exception e) {
			logger.error( seconds + " can't be formatted >>> " + e, e);
			throw(e);
		}
	}

/** ================================================ Validator Date =========================================== **/
    	
	/**
	 * Controlla se la stringa data in ingresso rappresenta una data valida secondo il formato 
	 * inserito.
	 * 
	 * @param dateToValidate - Stringa da valutare
 	 * @param dateFromat - Formato della data
	 * @return true se la stringa data in ingresso rappresenta una data valida, false altrimenti 
	 */
	@SuppressWarnings("unused")
	public static boolean isThisDateValid(String dateToValidate, String dateFormat){
		if(dateToValidate == null){
			return false;
		}

		SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
		sdf.setLenient(false);

		try {
			//if not valid, it will throw ParseException
			Date date = sdf.parse(dateToValidate);
		} catch (ParseException e) {
			return false;
		}

		return true;
	}
	
	/**
	 * Controlla se la stringa data in ingresso rappresenta un time valido 
	 * 
	 * @param timeToValidate - Stringa da valutare
	 * @return true se la stringa data in ingresso rappresenta un time valido, false altrimenti 
	 */
	public static boolean isThisTimeValid(String timeToValidate){
		try{
			if(timeToValidate == null) return false;

			LocalTime.parse(timeToValidate);
			
			return true;
		}catch(Exception e){
			return false;
		}
	}
    
/** ================================================== Operator LocalDateTime =================================================== **/
	 
	/**
	 * Ritorna la data/ora in millisecondi
	 * 
	 * @param date - {@link LocalDateTime}
	 * @return
	 * @throws Exception
	 */
	public static long getTimeMillis(LocalDateTime date) throws Exception{
		if(date == null)
			return -1;
		return DateTimeUtil.asUtilDate(date).getTime();
	}

	/** 
	 * Ritorna la data in millisecondi
	 * 
	 * @param date - {@link LocalDate}
	 * @return
	 * @throws Exception
	 */
	public static long getTimeMillis(LocalDate date) throws Exception{
		if(date == null)
			return -1;
		return DateTimeUtil.asUtilDate(date).getTime();
	}
	
	/**
	 * Restituisce una copia della data con reimpostati i campi connessi al time a zero!
	 *
	 * @param {@link LocalDateTime} date
	 *
	 * @return {@link LocalDateTime} con ora, minuti, secondi e millisecondi a ZERO!
	 */
	public static LocalDateTime zeroTime(LocalDateTime date) {
	    return date.with(LocalTime.MIDNIGHT);
	}
	
    /**
     * Restituisce una copia di questa {@code LocalDateTime} con il periodo specificato aggiunto o sottratto.
     *
     * @param {@link LocalDateTime} datetime
     * @param {@link long} months, i mesi da aggiungere
     * @param {@link long} days, i giorni da aggiungere
     * @param {@link long} hours, le ore da aggiungere
     * @param {@link long} minutes, i minuti da aggiungere
     * @param {@link long} seconds, i secondi da aggiungere
     * @param {@link long} sign, il segno per determinare se aggiungere o sottrarre (-1 sottrae, 1 aggiunge)
     * @return {@link LocalDateTime} 
     */
    public static LocalDateTime addOrSubtractLocalDateTime(LocalDateTime datetime, long months, long days, long hours, long minutes, long seconds, int sign) throws Exception {
    	try{
	    	if (datetime == null) return null;
		
	    	if ((months | days | hours | minutes | seconds) == 0) return datetime;
		        
	    	if (seconds != 0 && (sign < 0 || sign > 0)) {
	    		if(sign > 0)
	    			datetime = datetime.plusSeconds(seconds);
		        else
		        	datetime = datetime.minusSeconds(seconds);
		    }
	    	if (minutes != 0 && (sign < 0 || sign > 0)) {
	    		if(sign > 0)
	    			datetime = datetime.plusMinutes(minutes);
		        else
		        	datetime = datetime.minusMinutes(minutes);
		    }
	    	if (hours != 0 && (sign < 0 || sign > 0)) {
	    		if(sign > 0)
	    			datetime = datetime.plusHours(hours);
		        else
		        	datetime = datetime.minusHours(hours);
		    }
	    	if (days != 0 && (sign < 0 || sign > 0)) {
	    		if(sign > 0)
	    			datetime = datetime.plusDays(days);
		        else
		        	datetime = datetime.minusDays(days);
		    }
	        if (months != 0 && (sign < 0 || sign > 0)) {
	        	if(sign > 0)
	        		datetime = datetime.plusMonths(months);
		        else
		        	datetime = datetime.minusMonths(months);
	        }
	
	        return datetime;
    	}catch(Exception e){
			logger.error("addOrSubtractLocalDateTime >>>> " + e, e);
			throw(e);
		}
    }
    
    /**
     * Restituisce una copia di questa {@code LocalDate} con il periodo specificato aggiunto o sottratto.
     *
     * @param {@link LocalDate} datetime
     * @param {@link long} months, i mesi da aggiungere
     * @param {@link long} days, i giorni da aggiungere
     * @param {@link long} sign, il segno per determinare se aggiungere o sottrarre (-1 sottrae, 1 aggiunge)
     * @return {@link LocalDate} 
     */
    public static LocalDate addOrSubtractLocalDate(LocalDate date, long months, long days, int sign) throws Exception {
    	try{
	    	if (date == null) return null;
		
	    	if ((months | days ) == 0) return date;
		        
	    	if (days != 0 && (sign < 0 || sign > 0)) {
	    		if(sign > 0)
	    			date = date.plusDays(days);
		        else
		        	date = date.minusDays(days);
		    }
	        if (months != 0 && (sign < 0 || sign > 0)) {
	        	if(sign > 0)
	        		date = date.plusMonths(months);
		        else
		        	date = date.minusMonths(months);
	        }
	
	        return date;
    	}catch(Exception e){
			logger.error("addOrSubtractLocalDate >>>> " + e, e);
			throw(e);
		}
    }
    
/** ====================================== Current Date in java.xml.datatype.*  ===================================== **/

	/**
	 * Restituisce la data/ora corrente (es: 2014-04-28T16:00:49.455)
	 * 
	 * @return LocalDateTime
	 * @throws DatatypeConfigurationException 
	 */
	public static XMLGregorianCalendar nowAsXMLGregorianCalendar() throws DatatypeConfigurationException {
		try {
			GregorianCalendar gregorianCalendar = new GregorianCalendar();
			
			DatatypeFactory datatypeFactory = DatatypeFactory.newInstance();
			
			XMLGregorianCalendar now = datatypeFactory.newXMLGregorianCalendar(gregorianCalendar);
			
			return now;
		} catch (DatatypeConfigurationException e) {
			logger.error("nowAsXMLGregorianCalendar >>>> " + e, e);
			throw(e);
		}
	}
	
/** ============================================ java.time.* <<<--->>> java.xml.datatype ====================================== **/

	/**
	 * Restituisce la data fornita in ingresso come @XMLGregorianCalendar
	 * 
	 * @param {@link LocalDateTime} dateTime
	 * 
	 * @return {@link XMLGregorianCalendar}
	 * @throws Exception 
	 */
	public static XMLGregorianCalendar transformLocalDateTimeToXMLGregorianCalendar(LocalDateTime dateTime) throws Exception {
		try{	
			if (dateTime == null) return null;
			
			GregorianCalendar c = new GregorianCalendar();		c.setTime(asUtilDate(dateTime));
			
			XMLGregorianCalendar data = DatatypeFactory.newInstance().newXMLGregorianCalendar(c);

			data.setTimezone(DatatypeConstants.FIELD_UNDEFINED);
			data.setFractionalSecond(null);
		
			return data;
		} catch (DatatypeConfigurationException e) {
			logger.error("transformLocalDateTimeToXMLGregorianCalendar >>>> " + e, e);
			throw(e);
		}
	}
	
}
